#region File Description
//-----------------------------------------------------------------------------
// NetworkPredictionGame.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
#endregion

namespace NetworkPrediction
{
    /// <summary>
    /// Sample showing how to use prediction and smoothing to compensate
    /// for the effects of network latency, and for the low packet send
    /// rates needed to conserve network bandwidth.
    /// </summary>
    public class NetworkPredictionGame : Microsoft.Xna.Framework.Game
    {
        

        const int screenWidth = 1280;
        const int screenHeight = 720;

        const int maxGamers = 16;
        const int maxLocalGamers = 4;

        TimeSpan latency;


        // Graphics objects.
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont font;


        // Current and previous input states.
        KeyboardState currentKeyboardState;
        GamePadState currentGamePadState;

        KeyboardState previousKeyboardState;
        GamePadState previousGamePadState;


        // Network objects.
        NetworkSession networkSession;

        PacketWriter packetWriter = new PacketWriter();
        PacketReader packetReader = new PacketReader();
        
        string errorMessage;


        // What kind of network latency and packet loss are we simulating?
        enum NetworkQuality
        {
            Typical,    // 100 ms latency, 10% packet loss
            Poor,       // 200 ms latency, 20% packet loss
            Perfect,    // 0 latency, 0% packet loss
        }

        NetworkQuality networkQuality;

        // How often should we send network packets?
        int framesBetweenPackets = 6;

        // How recently did we send the last network packet?
        int framesSinceLastSend;

        // Is prediction and/or smoothing enabled?
        bool enablePrediction = true;
        bool enableSmoothing = true;




       

        public NetworkPredictionGame()
        {
            graphics = new GraphicsDeviceManager(this);

            graphics.PreferredBackBufferWidth = screenWidth;
            graphics.PreferredBackBufferHeight = screenHeight;

            Content.RootDirectory = "Content";
            Components.Add(new GamerServicesComponent(this));

            NetworkSession.InviteAccepted += InviteAcceptedEventHandler;
        }

        void InviteAcceptedEventHandler(object sender, InviteAcceptedEventArgs e)
        {
            DrawMessage("Joining session from invite...");

            // Leave the current network session.
            if (networkSession != null)
            {
                networkSession.Dispose();
                networkSession = null;
            }

            try
            {
                // Join a new session in response to the invite.
                networkSession = NetworkSession.JoinInvited(maxLocalGamers);
                HookSessionEvents();
            }
            catch (Exception error)
            {
                errorMessage = error.Message;
            }
        }


        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);         
            font = Content.Load<SpriteFont>("Font");
        }


       
        #region Update

 
        protected override void Update(GameTime gameTime)
        {
            HandleInput();

            if (networkSession == null)
            {
                // If we are not in a network session, update the
                // menu screen that will let us create or join one.
                UpdateMenuScreen();
            }
            else
            {
                // If we are in a network session, update it.
                UpdateNetworkSession(gameTime);
            }

            base.Update(gameTime);
        }
 
        void UpdateMenuScreen()
        {
            if (IsActive)
            {
                if (Gamer.SignedInGamers.Count == 0)
                {
                    // If there are no profiles signed in, we cannot proceed.
                    // Show the Guide so the user can sign in.
                    Guide.ShowSignIn(maxLocalGamers, false);
                }
                else if (IsPressed(Keys.A, Buttons.A))
                {
                    // Create a new session?
                    CreateSession();
                }
                else if (IsPressed(Keys.B, Buttons.B))
                {
                    // Join an existing session?
                    JoinSession();
                }
            }
        }

        void CreateSession()
        {
            DrawMessage("Creating session...");

            try
            {
                networkSession = NetworkSession.Create(NetworkSessionType.PlayerMatch,  maxLocalGamers, maxGamers);
                HookSessionEvents();
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }
        void JoinSession()
        {
            DrawMessage("Joining session...");

            try
            {
                // Search for sessions.
                using (AvailableNetworkSessionCollection availableSessions = NetworkSession.Find(NetworkSessionType.PlayerMatch, maxLocalGamers, null))
                {
                    if (availableSessions.Count == 0)
                    {
                        errorMessage = "No network sessions found.";
                        return;
                    }

                    // Join the first session we found.
                    networkSession = NetworkSession.Join(availableSessions[0]);

                    HookSessionEvents();
                }
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }

        void HookSessionEvents()
        {
            networkSession.GamerJoined += GamerJoinedEventHandler;
            networkSession.SessionEnded += SessionEndedEventHandler;
        }


        void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e)
        {
            int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer);
            e.Gamer.Tag = new Tank(gamerIndex, Content, screenWidth, screenHeight);
        }
        void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e)
        {
            errorMessage = e.EndReason.ToString();
            networkSession.Dispose();
            networkSession = null;
        }


        /// <summary>
        /// Updates the state of the network session, moving the tanks
        /// around and synchronizing their state over the network.
        /// </summary>
        void UpdateNetworkSession(GameTime gameTime)
        {
            // Is it time to send outgoing network packets?
            bool sendPacketThisFrame = false;

            framesSinceLastSend++;

            if (framesSinceLastSend >= framesBetweenPackets)
            {
                sendPacketThisFrame = true;
                framesSinceLastSend = 0;
            }

            // Update our locally controlled tanks, sending
            // their latest state at periodic intervals.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                UpdateLocalGamer(gamer, gameTime, sendPacketThisFrame);
            }

            // Pump the underlying session object.
            try
            {
                networkSession.Update();
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
                networkSession.Dispose();
                networkSession = null;
            }

            // Make sure the session has not ended.
            if (networkSession == null)   return;

            // Read any packets telling us the state of remotely controlled tanks.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                ReadIncomingPackets(gamer, gameTime);  // Prediction
            }

            // Apply prediction and smoothing to the remotely controlled tanks.
            foreach (NetworkGamer gamer in networkSession.RemoteGamers)
            {
                Tank tank = gamer.Tag as Tank;
                tank.UpdateRemote(framesBetweenPackets, enablePrediction);
            }

            // Update the latency and packet loss simulation options.
            UpdateOptions();
        }


        /// <summary>
        /// Helper for updating a locally controlled gamer.
        /// </summary>
        void UpdateLocalGamer(LocalNetworkGamer gamer, GameTime gameTime,    bool sendPacketThisFrame)
        {
            // Look up what tank is associated with this local player.
            Tank tank = gamer.Tag as Tank;

            // Read the inputs controlling this tank.
            PlayerIndex playerIndex = gamer.SignedInGamer.PlayerIndex;

            Vector2 tankInput;
            Vector2 turretInput;

            ReadTankInputs(playerIndex, out tankInput, out turretInput);

            // Update the tank.
            tank.UpdateLocal(tankInput, turretInput);

            // Periodically send our state to everyone in the session.
            if (sendPacketThisFrame)
            {
                tank.WriteNetworkPacket(packetWriter, gameTime);
                gamer.SendData(packetWriter, SendDataOptions.InOrder);
            }
        }


        /// <summary>
        /// Helper for reading incoming network packets.
        /// </summary>
        void ReadIncomingPackets(LocalNetworkGamer gamer, GameTime gameTime)
        {
            // Keep reading as long as incoming packets are available.
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                gamer.ReceiveData(packetReader, out sender);

                // Discard packets sent by local gamers: we already know their state!
                if (sender.IsLocal) continue;

                // Look up the tank associated with whoever sent this packet.
                Tank tank = sender.Tag as Tank;

                // Estimate how long this packet took to arrive.
                latency = networkSession.SimulatedLatency + TimeSpan.FromTicks(sender.RoundtripTime.Ticks / 2);

                // Read the state of this tank from the network packet.
                tank.ReadNetworkPacket(packetReader, gameTime, latency,  enablePrediction, enableSmoothing);
            }
        }


        /// <summary>
        /// Updates the latency and packet loss simulation options. Only the
        /// host can alter these values, which are then synchronized over the
        /// network by storing them into NetworkSession.SessionProperties. Any
        /// changes to the SessionProperties data are automatically replicated
        /// on all the client machines, so there is no need to manually send
        /// network packets to transmit this data.
        /// </summary>
        void UpdateOptions()
        {
            if (networkSession.IsHost)
            {
                // Change the network quality simultation?
                if (IsPressed(Keys.A, Buttons.A))
                {
                    networkQuality++;
                    if (networkQuality > NetworkQuality.Perfect) networkQuality = 0;
                }

                // Change the packet send rate?
                if (IsPressed(Keys.B, Buttons.B))
                {
                    if (framesBetweenPackets == 6) framesBetweenPackets = 3;
                    else if (framesBetweenPackets == 3) framesBetweenPackets = 1;
                    else  framesBetweenPackets = 6;
                }

                // Toggle prediction on or off?
                if (IsPressed(Keys.X, Buttons.X)) enablePrediction = !enablePrediction;

                // Toggle smoothing on or off?
                if (IsPressed(Keys.Y, Buttons.Y)) enableSmoothing = !enableSmoothing;

                // Stores the latest settings into NetworkSession.SessionProperties.
                networkSession.SessionProperties[0] = (int)networkQuality;
                networkSession.SessionProperties[1] = framesBetweenPackets;
                networkSession.SessionProperties[2] = enablePrediction ? 1 : 0;
                networkSession.SessionProperties[3] = enableSmoothing ? 1 : 0;
            }
            else
            {
                // Client machines read the latest settings from the session properties.
                networkQuality = (NetworkQuality)networkSession.SessionProperties[0];
                framesBetweenPackets = networkSession.SessionProperties[1].Value;
                enablePrediction = networkSession.SessionProperties[2] != 0;
                enableSmoothing = networkSession.SessionProperties[3] != 0;
            }

            // Update the SimulatedLatency and SimulatedPacketLoss properties.
            switch (networkQuality)
            {
                case NetworkQuality.Typical:
                    networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(100);
                    networkSession.SimulatedPacketLoss = 0.1f;
                    break;

                case NetworkQuality.Poor:
                    networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(200);
                    networkSession.SimulatedPacketLoss = 0.2f;
                    break;

                case NetworkQuality.Perfect:
                    networkSession.SimulatedLatency = TimeSpan.Zero;
                    networkSession.SimulatedPacketLoss = 0;
                    break;
            }
        }


        #endregion

        #region Draw

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            if (networkSession == null)
            {
                // If we are not in a network session, draw the
                // menu screen that will let us create or join one.
                DrawMenuScreen();
            }
            else
            {
                // If we are in a network session, draw it.
                DrawNetworkSession();
            }

            base.Draw(gameTime);
        }



        void DrawMenuScreen()
        {
            string message = string.Empty;

            if (!string.IsNullOrEmpty(errorMessage))  message += "Error:\n" + errorMessage.Replace(". ", ".\n") + "\n\n";

            message += "A = create session\n" + "B = join session";

            spriteBatch.Begin();
            spriteBatch.DrawString(font, message, new Vector2(60, 160), Color.White);       
            spriteBatch.End();
        }


        void DrawNetworkSession()
        {
            spriteBatch.Begin();

            DrawOptions();

            // For each person in the session...
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                // Look up the tank object belonging to this network gamer.
                Tank tank = gamer.Tag as Tank;

                // Draw the tank.
                tank.Draw(spriteBatch);

                // Draw a gamertag label.
                spriteBatch.DrawString(font, gamer.Gamertag, tank.Position,Color.Black, 0, new Vector2(100, 150), 0.6f, SpriteEffects.None, 0);
            }

            spriteBatch.End();
        }

        void DrawOptions()
        {
            string quality =  string.Format  ("Corruption = {0} ms, {1}% Loss",networkSession.SimulatedLatency.TotalMilliseconds, networkSession.SimulatedPacketLoss * 100);

            string sendRate = string.Format  ("PacketsPPS = {0}",60 / framesBetweenPackets);

            string prediction = string.Format("Prediction = {0}",enablePrediction ? "1" : "0");

            string smoothing = string.Format ("Smoothings = {0}", enableSmoothing ? "1" : "0");

            // If we are the host, include prompts telling how to change the settings.
            // if (networkSession.IsHost)
            // {
            //   quality += " (A to change)";
            //   sendRate += " (B to change)";
            //   prediction += " (X to toggle)";
            //   smoothing += " (Y to toggle)";
            //  }

            // Draw combined text to the screen.
            string message = quality + "\n" +
                             sendRate + "\n" +
                             prediction + "\n" +
                             smoothing;

            spriteBatch.DrawString(font, message, new Vector2(50, 10), Color.White);

            spriteBatch.DrawString(font, ((int)latency.Milliseconds).ToString(), new Vector2(630, 600), Color.White);

        }


        void DrawMessage(string message)
        {
            if (!BeginDraw()) return;
            GraphicsDevice.Clear(Color.DarkRed);
            spriteBatch.Begin();
            spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);
            spriteBatch.End();
            EndDraw();
        }

        
        #endregion

        #region Handle Input


        /// <summary>
        /// Handles input.
        /// </summary>
        private void HandleInput()
        {
  
            previousGamePadState = currentGamePadState;
            currentGamePadState = GamePad.GetState(PlayerIndex.One);

            if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
            {
                Exit();
            }
        }

        bool IsPressed(Keys key, Buttons button)
        {
            return ((currentKeyboardState.IsKeyDown(key) &&
                     previousKeyboardState.IsKeyUp(key)) ||
                    (currentGamePadState.IsButtonDown(button) &&
                     previousGamePadState.IsButtonUp(button)));
        }


        static void ReadTankInputs(PlayerIndex playerIndex, out Vector2 tankInput, out Vector2 turretInput)
        {
            // Read the gamepad.
            GamePadState gamePad = GamePad.GetState(playerIndex);

            tankInput = gamePad.ThumbSticks.Left;
            turretInput = gamePad.ThumbSticks.Right;

            // Normalize the input vectors.
            if (tankInput.Length() > 1) tankInput.Normalize();
            if (turretInput.Length() > 1) turretInput.Normalize();
        }


        #endregion
    }


    #region Entry Point

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static class Program
    {
        static void Main()
        {
            using (NetworkPredictionGame game = new NetworkPredictionGame())
            {
                game.Run();
            }
        }
    }

    #endregion
}
